/*******************************************************************************
 * Copyright (c) 2001, 2002 Boris Pruessmann and others. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Common Public License v0.5 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/cpl-v05.html
 * 
 * Contributors: Boris Pruessmann - Initial implementation.
 ******************************************************************************/
package net.sourceforge.perforce.core;

import com.google.common.collect.Lists;

import net.sourceforge.perforce.core.api.IPerforceListener;
import net.sourceforge.perforce.core.api.IPerforceServer;
import net.sourceforge.perforce.core.api.IPerforceServerManager;
import net.sourceforge.perforce.core.internal.CacheStore;
import net.sourceforge.perforce.core.internal.Policy;
import net.sourceforge.perforce.core.internal.events.ResourceStateDelta;
import net.sourceforge.perforce.core.internal.utils.MetaFileWriter;
import net.sourceforge.perforce.core.syncinfo.FolderSyncInfo;
import net.sourceforge.perforce.core.syncinfo.IResourceStateDelta;
import net.sourceforge.perforce.core.syncinfo.ReentrantLock;
import net.sourceforge.perforce.core.syncinfo.ResourceSyncInfo;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;

import java.io.Externalizable;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * @version $Revision: 488 $
 * @author <a href="mailto:bpruessmann@users.sourceforge.net">Boris
 *         Pruessmann</a>
 */
public class PerforceSynchronizer implements IPerforceListener {
  // ---------------------------------------------------------------------------
  // Private Constants

  /** Number of entries in LRU list */
  private static final int LRU_THRESHOLD = 10;

  /** Used as identifier for project folder sync info persistent property. */
  private static final QualifiedName PROJECT_SYNC_KEY =
      new QualifiedName(PerforceCorePlugin.ID, "project-sync"); //$NON-NLS-1$

  /** Used as identifier for folder sync info persistent property. */
  private static final QualifiedName FOLDER_SYNC_KEY =
      new QualifiedName(PerforceCorePlugin.ID, "folder-sync"); //$NON-NLS-1$

  /** Used as identifier for resource sync info persistent property. */
  private static final QualifiedName RESOURCE_SYNC_KEY =
      new QualifiedName(PerforceCorePlugin.ID, "resource-sync"); //$NON-NLS-1$

  /** Used as identifier for ignored resources persistent property. */
  private static final QualifiedName IGNORE_SYNC_KEY =
      new QualifiedName(PerforceCorePlugin.ID, "folder-ignore"); //$NON-NLS-1$

  /**
   * Used as indicator for folder sync info that does not exist (in comparison
   * to not yet fetched)
   */
  private static final FolderSyncInfo NULL_FOLDER_SYNC_INFO = new FolderSyncInfo(""); //$NON-NLS-1$

  /** Ok Status used during endOperation() */
  private static final IStatus STATUS_OK =
      new Status(IStatus.OK, PerforceCorePlugin.ID, 0, Policy.bind("ok"), null); //$NON-NLS-1$

  /** Constant indicating the result when there are no ignores. */
  private static final String[] NULL_IGNORES = new String[0];

  // --------------------------------------------------------------------
  // Private Static Variables

  /** Stores the PerforceSynchronizer singleton . */
  private static PerforceSynchronizer instance;

  // ---------------------------------------------------------------------------
  // Private Variables

  /** Used to achieve reentrance. */
  private ReentrantLock lock = new ReentrantLock();
  /** Maps changed resources to the new ResourceSyncInfo. */
  private Map<IResource, ResourceSyncInfo> changedResourceSync =
      new HashMap<IResource, ResourceSyncInfo>();
  /** Maps changed fodlers to the new FolderSyncInfo. */
  private Map<IContainer, FolderSyncInfo> changedFolderSync =
      new HashMap<IContainer, FolderSyncInfo>();
  /** Maps servers to their CacheStores. */
  private Map<IPerforceServer, CacheStore> serverToCacheStore =
      new HashMap<IPerforceServer, CacheStore>();
  /** List that stores the LRU maps to make sure they aren't garbage collected. */
  private List<Map<String, Externalizable>> lruMaps = new LinkedList<Map<String, Externalizable>>();
  /** Indicates whether we should output debug information. */
  private final boolean debug;

  //----------------------------------------------------------------------------
  // ---- Constructors

  /** Constructor for PerforceSynchronizer. */
  private PerforceSynchronizer() {
    final String value = Platform.getDebugOption(PerforceCorePlugin.ID + "/cache");//$NON-NLS-1$
    debug = PerforceCorePlugin.getPlugin().isDebugging() && Boolean.valueOf(value).booleanValue();
  }

  // -----------------------------------------------------------------------
  // Public Static Methods

  /**
   * Returns the PerforceSynchronizer singleton instance.
   * 
   * @return the singleton instance.
   * 
   * @pre this.instance != null
   */
  public static PerforceSynchronizer getInstance() {
    if (instance == null) {
      instance = new PerforceSynchronizer();
    }

    return instance;
  }

  //----------------------------------------------------------------------------
  // -- Public Methods

  /**
   * Returns the startup status of the PerforceSynchronizer. Merges the status
   * of all Perforce servers known to ServerManager.
   * 
   * @return merged status of Perforce servers
   */
  public IStatus getStatus() {
    MultiStatus status =
        new MultiStatus(PerforceCorePlugin.ID, IStatus.OK, Policy
            .bind("PerforceSynchronizer.startupError"), //$NON-NLS-1$
            null);

    try {
      lock.acquire();

      final IPerforceServer[] servers = PerforceCorePlugin.getServerManager().getPerforceServers();
      for (int i = 0; i < servers.length; i++) {
        final IPerforceServer server = servers[i];
        if (!serverToCacheStore.containsKey(server)) {
          final CacheStore cacheStore = new CacheStore(server);
          serverToCacheStore.put(server, cacheStore);
        }
      }
      for (Iterator<CacheStore> it = serverToCacheStore.values().iterator(); it.hasNext();) {
        CacheStore cacheStore = it.next();
        status.merge(cacheStore.getStatus());
      }
    } finally {
      lock.release();
    }
    return status;
  }

  /**
   * Call this method to start the PerforceSynchronizer.
   * 
   * Has to be performed before calling getInstance() for the first time.
   */
  public void startup() {
    PerforceCorePlugin.getServerManager().addPerforceListener(this);
  }

  /** Call this method for shutting down the PerforceSynchronizer. */
  public void shutdown() {
    PerforceCorePlugin.getServerManager().removePerforceListener(this);
    try {
      lock.acquire();
      for (Iterator<CacheStore> it = serverToCacheStore.values().iterator(); it.hasNext();) {
        CacheStore cacheStore = it.next();
        try {
          cacheStore.close();
        } catch (IOException e) {
          PerforceCorePlugin.logError("Failed to close CacheStore.", e);
        }
      }
    } finally {
      lock.release();
    }
  }

  /**
   * Begins a batch of operations.
   * 
   * @param monitor the progress monitor, may be null
   */
  public void beginOperation(IProgressMonitor monitor) {
    lock.acquire();
    if (lock.getNestingCount() == 1) {
      prepareCache();
    }
  }

  /**
   * Ends a batch of operations. Performs broadcast of changes to registered
   * listeners.
   * 
   * @param monitor the progress monitor, may be null.
   * @throws PerforceException in case of an unexpected error.
   */
  public void endOperation(IProgressMonitor monitor) throws PerforceException {
    try {
      IStatus status = STATUS_OK;
      if (lock.getNestingCount() == 1) {
        if (!changedFolderSync.isEmpty() || !changedResourceSync.isEmpty()) {
          status = commitCache(monitor);
        }
      }

      if (status != STATUS_OK && !status.isOK()) {
        throw new PerforceException(status);
      }
    } finally {
      lock.release();
    }
  }

  /**
   * Call this method to retrieve the FolderSyncInfo of the given container.
   * 
   * @param folder the container for which FolderSyncInfo is needed.
   * @return the requested FolderSyncInfo or null if the folder isn't synced.
   * 
   * @throws PerforceException in case of an error.
   * 
   * @pre folder != null
   * 
   *      TODO: Cleanup
   */
  public FolderSyncInfo getFolderSync(IContainer folder) throws PerforceException {
    Policy.assertNotNull(folder);

    if (folder.getType() == IResource.ROOT || !folder.exists()) {
      return null;
    }

    if (folder.getType() == IResource.PROJECT) {
      try {
        beginOperation(null);
        return getCachedProjectSync((IProject) folder);
      } finally {
        endOperation(null);
      }
    }

    IContainer parent = folder.getParent();
    if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT) {
      return null;
    }

    try {
      beginOperation(null);

      cacheFolderSyncForChildren(parent);
      return getCachedFolderSync(folder);
    } finally {
      endOperation(null);
    }
  }

  /**
   * Sets the FolderSyncInfo of the given folder.
   * 
   * @param folder the folder.
   * @param syncInfo the new sync info.
   * 
   * @throws PerforceException in case of an unexpected error.
   */
  public void setFolderSync(IContainer folder, FolderSyncInfo syncInfo) throws PerforceException {
    if (folder.getType() == IResource.ROOT || !folder.exists()) {
      return;
    }

    if (folder.getType() == IResource.PROJECT) {
      try {
        beginOperation(null);
        setCachedProjectSync((IProject) folder, syncInfo);
        return;
      } finally {
        endOperation(null);
      }
    }

    IContainer parent = folder.getParent();
    if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT) {
      return;
    }

    try {
      beginOperation(null);

      cacheFolderSyncForChildren(parent);
      setCachedFolderSync(folder, syncInfo);
    } finally {
      endOperation(null);
    }
  }

  /**
   * Deletes the FolderSyncInfo for the given container.
   * 
   * @param container the container.
   * 
   * @throws PerforceException in case of an error.
   * 
   * @pre container != null
   */
  public void deleteFolderSync(IContainer container) throws PerforceException {
    Policy.assertNotNull(container);

    IContainer parent = container.getParent();
    if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT) {
      return;
    }

    if (container.getType() == IResource.PROJECT) {
      try {
        beginOperation(null);
        removeProjectSyncInfo((IProject) container);
      } finally {
        endOperation(null);
      }
    }

    try {
      beginOperation(null);

      cacheFolderSyncForChildren(parent);
      removeFolderSyncInfo(container);
    } finally {
      endOperation(null);
    }
    if (container.getType() == IResource.ROOT || !container.exists()) {
      return;
    }
  }

  /**
   * Call this method to request the ResourceSyncInfo of the given resource.
   * 
   * @param file the IResource for which ResourceSyncInfo is requested.
   * @return the requested ResourceSyncInfo.
   * 
   * @throws PerforceException in case of an exceptional situation.
   * 
   * @pre file != null
   */
  public ResourceSyncInfo getResourceSync(IResource file) throws PerforceException {
    Policy.assertNotNull(file);

    IContainer parent = file.getParent();
    if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT) {
      return null;
    }

    try {
      beginOperation(null);

      cacheResourceSyncForChildren(parent);
      return getCachedResourceSync(file);
    } finally {
      endOperation(null);
    }
  }

  /**
   * Changes the ResourceSyncInfo for the given resource.
   * 
   * @param resource the resource to change.
   * @param syncInfo the new ResourceSyncInfo.
   * 
   * @throws PerforceException in case of an error.
   * 
   * @pre resource != null
   * @pre syncInfo != null
   */
  public void setResourceSync(IResource resource, ResourceSyncInfo syncInfo)
      throws PerforceException {
    Policy.assertNotNull(resource);
    Policy.assertNotNull(syncInfo);

    IContainer parent = resource.getParent();
    if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT) {
      return;
    }

    try {
      beginOperation(null);

      cacheResourceSyncForChildren(parent);
      setCachedResourceSync(resource, syncInfo);
    } finally {
      endOperation(null);
    }
  }

  /**
   * Deletes the ResourceSyncInfo of the given resource.
   * 
   * @param resource the resource.
   * 
   * @throws PerforceException in case of an error.
   * 
   * @pre container != null
   */
  public void deleteResourceSync(IResource resource) throws PerforceException {
    Policy.assertNotNull(resource);

    IContainer parent = resource.getParent();
    if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT) {
      return;
    }

    try {
      beginOperation(null);

      cacheResourceSyncForChildren(parent);
      removeResourceSyncInfo(resource);
    } finally {
      endOperation(null);
    }
  }

  /**
   * Call this method to retrieve the ignore patterns for the given container.
   * 
   * @param container the container.
   * @return array of ignore patterns.
   * @throws PerforceException in case of an unexpected error.
   */
  public String[] getIgnored(IContainer container) throws PerforceException {
    if (container.getType() == IResource.ROOT || !container.exists()) {
      return NULL_IGNORES;
    }

    try {
      beginOperation(null);

      return cacheFolderIgnores(container);
    } finally {
      endOperation(null);
    }
  }

  /**
   * Adds a pattern to the set of ignores for the specified folder.
   * 
   * @param folder the folder
   * @param pattern the pattern
   * @throws PerforceException in case of an error.
   */
  public void addIgnored(IContainer folder, String pattern) throws PerforceException {
    Policy.assertNotNull(folder);
    Policy.assertNotNull(pattern);

    if (folder.getType() == IResource.ROOT || !folder.exists()) {
      return;
    }

    try {
      beginOperation(null);
      String[] ignores = MetaFileWriter.readP4IgnoreEntries(folder);
      if (ignores != null) {
        // verify that the pattern has not already been added
        for (int i = 0; i < ignores.length; i++) {
          if (ignores[i].equals(pattern)) {
            return;
          }
        }

        // add the pattern
        String[] oldIgnores = ignores;
        ignores = new String[oldIgnores.length + 1];
        System.arraycopy(oldIgnores, 0, ignores, 0, oldIgnores.length);
        ignores[oldIgnores.length] = pattern;
      } else {
        ignores = new String[] {pattern};
      }
      setCachedFolderIgnores(folder, ignores);
      MetaFileWriter.writeP4IgnoreEntries(folder, ignores);

      List<ResourceStateDelta> possibleIgnores = Lists.newArrayList();
      accumulateNonManagedChildren(folder, possibleIgnores);

      final IResourceStateDelta[] deltas = new IResourceStateDelta[possibleIgnores.size()];
      possibleIgnores.toArray(deltas);

      PerforceCorePlugin.getPlugin().broadcastResourceStateChanges(deltas);
    } finally {
      endOperation(null);
    }
  }

  /**
   * Called to indicate that the meta files of the given containers have
   * changed.
   * 
   * @param roots the containers.
   * @throws PerforceException in case of an unexpected error.
   */
  public void metaFilesChanged(IContainer[] roots) throws PerforceException {
    try {
      for (int i = 0; i < roots.length; i++) {
        IContainer root = roots[i];
        root.setSessionProperty(IGNORE_SYNC_KEY, null);

        List<ResourceStateDelta> possibleIgnores = new ArrayList<ResourceStateDelta>();
        accumulateNonManagedChildren(root, possibleIgnores);

        IResourceStateDelta[] deltas = new IResourceStateDelta[possibleIgnores.size()];
        possibleIgnores.toArray(deltas);

        PerforceCorePlugin.getPlugin().broadcastResourceStateChanges(deltas);
      }
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }

  }

  // ---------------------------------------------------------- Public Methods
  // (IPerforceListener)

  /** @see IPerforceListener#perforceServerAdded(IPerforceServer) */
  public void perforceServerAdded(IPerforceServer server) {
  }

  /** @see IPerforceListener#perforceServerRemoved(IPerforceServer) */
  public void perforceServerRemoved(IPerforceServer server) {
    CacheStore store = serverToCacheStore.get(server);
    if (store == null) return;

    try {
      store.close();
    } catch (IOException e) {
      PerforceCorePlugin.logError("Error closing the CacheStore.", e); //$NON-NLS-1$
    } finally {
      store.delete();
    }
  }

  //----------------------------------------------------------------------------
  // - Private Methods

  /**
   * Recursively adds to the possibleIgnores list all children of the given
   * folder that can be ignored/unignored.
   * 
   * @param folder the folder to be searched
   * @param possibleIgnores the list of IResources that can be ignored
   * @throws PerforceException in case of an error.
   */
  private void accumulateNonManagedChildren(IContainer folder,
      List<ResourceStateDelta> possibleIgnores) throws PerforceException {
    try {
      cacheResourceSyncForChildren(folder);
      IResource[] children = folder.members();
      List<IResource> folders = new ArrayList<IResource>();

      for (int i = 0; i < children.length; i++) {
        IResource child = children[i];

        if (child.getType() != IResource.FILE) {
          possibleIgnores
              .add(new ResourceStateDelta(IResourceStateDelta.CHANGED, child, null, null));
          folders.add(child);
        } else {
          ResourceSyncInfo resourceSync = getResourceSync(child);
          possibleIgnores.add(new ResourceStateDelta(IResourceStateDelta.CHANGED, child,
              resourceSync, resourceSync));
        }
      }

      for (Iterator<IResource> it = folders.iterator(); it.hasNext();) {
        IContainer childFolder = (IContainer) it.next();
        accumulateNonManagedChildren(childFolder, possibleIgnores);
      }
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }
  }

  /**
   * Removes the FolderSyncInfo of the given project.
   * 
   * @param project the container.
   * 
   * @throws PerforceException in case of an error.
   */
  private void removeProjectSyncInfo(IProject project) throws PerforceException {
    IContainer parent = project.getParent();
    if (parent == null) {
      return;
    }

    if (getCachedProjectSync(project) != null) {
      setCachedProjectSync(project, null);
    }
  }

  /**
   * Removes the FolderSyncInfo of the given container.
   * 
   * @param container the container.
   * 
   * @throws PerforceException in case of an error.
   */
  private void removeFolderSyncInfo(IContainer container) throws PerforceException {
    IContainer parent = container.getParent();
    if (parent == null) {
      return;
    }

    cacheFolderSyncForChildren(parent);
    if (getCachedFolderSync(container) != null) {
      setCachedFolderSync(container, null);
    }
  }

  /**
   * Removes the ResourceSyncInfo of the given resource.
   * 
   * @param file the resource.
   * 
   * @throws PerforceException in case of an error.
   */
  private void removeResourceSyncInfo(IResource file) throws PerforceException {
    IContainer parent = file.getParent();
    if (parent == null || !parent.exists() || parent.getType() == IResource.ROOT) {
      return;
    }

    cacheResourceSyncForChildren(parent);
    if (getCachedResourceSync(file) != null) {
      setCachedResourceSync(file, null);
    }
  }

  /**
   * Sets the cached folder sync info for the given project.
   * 
   * @param project the container.
   * @param syncInfo the new FolderSyncInfo.
   * @throws PerforceException in case of an unexpected error.
   */
  private void setCachedProjectSync(IProject project, FolderSyncInfo syncInfo)
      throws PerforceException {
    try {
      if (syncInfo == null) {
        project.setSessionProperty(PROJECT_SYNC_KEY, null);
        changedFolderSync.put(project, null);
      } else {
        project.setSessionProperty(PROJECT_SYNC_KEY, syncInfo);
        changedFolderSync.put(project, syncInfo);
      }
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }
  }

  /**
   * Sets the cached folder sync info.
   * 
   * @param container the container.
   * @param syncInfo the new FolderSyncInfo.
   * @throws PerforceException in case of an unexpected error.
   */
  private void setCachedFolderSync(IContainer container, FolderSyncInfo syncInfo)
      throws PerforceException {
    IContainer parent = container.getParent();
    Map<String, Externalizable> children = getMapFromSessionProperty(parent, FOLDER_SYNC_KEY);
    Policy.assertNotNull(children);

    if (syncInfo == null) {
      children.remove(container.getName());
      changedFolderSync.put(container, null);
    } else {
      children.put(container.getName(), syncInfo);
      changedFolderSync.put(container, syncInfo);
    }
  }

  /**
   * Called to change the chached ResourceSyncInfo of the given resource.
   * 
   * @param resource the resource.
   * @param syncInfo the new ResourceSyncInfo.
   * 
   * @throws PerforceException if attaching the new persistent property fails.
   */
  private void setCachedResourceSync(IResource resource, ResourceSyncInfo syncInfo)
      throws PerforceException {
    IContainer parent = resource.getParent();
    Map<String, Externalizable> children = getMapFromSessionProperty(parent, RESOURCE_SYNC_KEY);
    Policy.assertNotNull(children);

    if (syncInfo == null) {
      children.remove(resource.getName());
      changedResourceSync.put(resource, null);
    } else {
      ResourceSyncInfo oldSyncInfo = (ResourceSyncInfo) children.get(resource.getName());
      if (oldSyncInfo == null || oldSyncInfo.getTimestamp() <= syncInfo.getTimestamp()) {
        children.put(resource.getName(), syncInfo);
        changedResourceSync.put(resource, syncInfo);
      }
    }
  }

  /** Prepares the cache. */
  private void prepareCache() {
    changedResourceSync.clear();
    changedFolderSync.clear();
  }

  /**
   * Commit changes to the cache.
   * 
   * @param monitor for progress feedback.
   * 
   * @return the status of the operation.
   */
  private IStatus commitCache(IProgressMonitor monitor) {
    long startTime = 0;
    if (debug) {
      startTime = System.currentTimeMillis();
      System.out.println();
      System.out.println("Started calculating resource state changes " + new Date());
    }

    IStatus result = STATUS_OK;

    int total = changedFolderSync.size() + changedResourceSync.size();
    List<ResourceStateDelta> resourceStateDeltas = new ArrayList<ResourceStateDelta>(total);

    if (debug) {
      System.out.println("Calculating folder changes... " + new Date());
    }

    monitor = Policy.monitorFor(monitor);
    monitor.subTask(Policy.bind("PerforceSynchronizer.refreshingCache")); //$NON-NLS-1$

    try {
      monitor = Policy.monitorFor(monitor);

      IPerforceServerManager manager = PerforceCorePlugin.getServerManager();

      monitor.beginTask("", total * 500 + serverToCacheStore.size() * 2000);

      for (Map.Entry<IContainer, FolderSyncInfo> entry : changedFolderSync.entrySet()) {
        IContainer container = entry.getKey();
        FolderSyncInfo syncInfo = entry.getValue();

        CacheStore cacheStore = getCacheStoreForResource(container);
        FolderSyncInfo oldSyncInfo = null;
        if (container.getType() == IResource.PROJECT) {
          oldSyncInfo = cacheStore.setProjectSyncInfo((IProject) container, syncInfo);
        } else {
          oldSyncInfo = cacheStore.setFolderSyncInfo(container, syncInfo);
        }

        int kind = IResourceStateDelta.CHANGED;
        if (syncInfo == null) {
          kind = IResourceStateDelta.REMOVED;
        } else if (oldSyncInfo == null) {
          kind = IResourceStateDelta.ADDED;
        }

        if (syncInfo != null || oldSyncInfo != null) {
          if (debug) {
            System.out.print("Resource state changed for "); //$NON-NLS-1$
            System.out.println(container.getLocation().toOSString());
          }

          resourceStateDeltas.add(new ResourceStateDelta(kind, container, null, null));
        } else {
          if (debug) {
            System.out.print("Resource state didn't change. Skipping "); //$NON-NLS-1$
            System.out.println(container.getLocation().toOSString());
          }
        }

        monitor.worked(500);
      }

      if (debug) {
        System.out.println("Calculating file changes... " + new Date());
      }

      for (Map.Entry<IResource, ResourceSyncInfo> entry : changedResourceSync.entrySet()) {
        final IResource resource = entry.getKey();
        final ResourceSyncInfo syncInfo = entry.getValue();

        CacheStore cacheStore = getCacheStoreForResource(resource);
        ResourceSyncInfo oldSyncInfo = cacheStore.setResourceSyncInfo(resource, syncInfo);

        int kind = 0;
        if (syncInfo == null) {
          kind = IResourceStateDelta.REMOVED;
        } else if (oldSyncInfo == null) {
          kind = IResourceStateDelta.ADDED;
        } else if (!syncInfo.equals(oldSyncInfo)) {
          kind = IResourceStateDelta.CHANGED;
        }

        if (kind != 0) {
          if (debug) {
            System.out.print("Resource state changed for "); //$NON-NLS-1$
            System.out.println(resource.getLocation().toOSString());
          }

          resourceStateDeltas.add(new ResourceStateDelta(kind, resource, syncInfo, oldSyncInfo));
        } else {
          if (debug) {
            System.out.print("Resource state didn't change. Skipping "); //$NON-NLS-1$
            System.out.println(resource.getLocation().toOSString());
          }
        }

        monitor.worked(500);
      }
    } catch (PerforceException e) {
      result = e.getStatus();
    }

    if (debug) {
      System.out.println("Finished calculating resource state changes " + new Date());
    }

    monitor.subTask("Committing cache...");
    for (Iterator<CacheStore> it = serverToCacheStore.values().iterator(); it.hasNext();) {
      try {
        CacheStore cacheStore = it.next();
        cacheStore.commit();

        monitor.worked(2000);
      } catch (IOException e) {
        PerforceCorePlugin.logError("Failed to commit CacheStore.", e);
      }
    }

    // Broadcast changes
    if (resourceStateDeltas.size() > 0) {
      final IResourceStateDelta[] deltas = new IResourceStateDelta[resourceStateDeltas.size()];
      resourceStateDeltas.toArray(deltas);

      // Eliminate one level of threading. Let's hope it makes a difference.
      // Runnable r = new Runnable()
      // {
      // public void run()
      // {
      PerforceCorePlugin.getPlugin().broadcastResourceStateChanges(deltas);
      // }
      // };

      // new Thread(r).start();
    }

    changedFolderSync.clear();
    changedFolderSync.clear();

    monitor.done();

    if (debug) {
      float timeDiff = System.currentTimeMillis() - startTime;
      float totalSeconds = (timeDiff) / 1000;
      System.out.println("commitCache(): total time = " + totalSeconds + " secs.");
    }
    return result;
  }

  /**
   * Called to retrieve cached FolderSyncInfo for the given project.
   * 
   * @param project the project.
   * @return the associated FolderSyncInfo
   * 
   * @throws PerforceException in case of an error.
   */
  private FolderSyncInfo getCachedProjectSync(IProject project) throws PerforceException {
    try {
      FolderSyncInfo result = (FolderSyncInfo) project.getSessionProperty(PROJECT_SYNC_KEY);

      if (result == null) {
        CacheStore cacheStore = getCacheStoreForResource(project);
        result = cacheStore.getProjectSyncInfo(project);
        if (result == null) {
          result = NULL_FOLDER_SYNC_INFO;
        }

        project.setSessionProperty(PROJECT_SYNC_KEY, result);
      }

      return result;
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }
  }

  /**
   * This method is called to cache the FolderSyncInfo for the children of the
   * specified container.
   * 
   * @param container the container.
   * 
   * @throws PerforceException when attaching the persistent property fails.
   */
  private void cacheFolderSyncForChildren(IContainer container) throws PerforceException {
    Map<String, Externalizable> children = getMapFromSessionProperty(container, FOLDER_SYNC_KEY);
    if (children == null) {
      CacheStore cacheStore = getCacheStoreForResource(container);

      FolderSyncInfo[] infos = cacheStore.getFolderSyncInfo(container);
      if (infos != null) {
        children = new HashMap<String, Externalizable>(infos.length);
        for (int i = 0; i < infos.length; i++) {
          FolderSyncInfo syncInfo = infos[i];
          children.put(syncInfo.getName(), syncInfo);
        }
      } else {
        children = new HashMap<String, Externalizable>(0);
      }

      putMapIntoSessionProperty(container, FOLDER_SYNC_KEY, children);
    }
  }

  /**
   * This method is being called to cache the ResourceSyncInfo for the children
   * of the given container.
   * 
   * @param container the container.
   * 
   * @throws PerforceException when attaching the persistent property fails.
   */
  private void cacheResourceSyncForChildren(IContainer container) throws PerforceException {
    Map<String, Externalizable> children = getMapFromSessionProperty(container, RESOURCE_SYNC_KEY);
    if (children == null) {
      CacheStore cacheStore = getCacheStoreForResource(container);

      ResourceSyncInfo[] infos = cacheStore.getResourceSyncInfo(container);
      if (infos != null) {
        children = new HashMap<String, Externalizable>(infos.length);
        for (int i = 0; i < infos.length; i++) {
          ResourceSyncInfo syncInfo = infos[i];
          children.put(syncInfo.getName(), syncInfo);
        }
      } else {
        children = new HashMap<String, Externalizable>(0);
      }

      putMapIntoSessionProperty(container, RESOURCE_SYNC_KEY, children);
    }
  }

  /**
   * Returns the CacheStore for the given resource.
   * 
   * @param resource the resource.
   * @return the CacheStore.
   * @throws PerforceException in case of an error.
   */
  private CacheStore getCacheStoreForResource(IResource resource) throws PerforceException {
    IPerforceServerManager manager = PerforceCorePlugin.getServerManager();
    IPerforceServer server = manager.getPerforceServer(resource);
    if (server == null) {
      Status status =
          new Status(Status.ERROR, PerforceCorePlugin.ID, Status.ERROR,
              "Failed to find PerforceServer for " + resource.toString(), null); //$NON-NLS-1$
      throw new PerforceException(status);
    }

    CacheStore store = serverToCacheStore.get(server);
    if (store == null) {
      store = new CacheStore(server);
      serverToCacheStore.put(server, store);
    }
    return store;
  }

  /**
   * Called to store the given map as session property of the specified
   * container.
   * 
   * @param container the container.
   * @param key the key of the property.
   * @param map the map to store.
   * @throws PerforceException in case of an error.
   */
  private void putMapIntoSessionProperty(IContainer container, QualifiedName key,
      Map<String, Externalizable> map) throws PerforceException {
    try {
      container.setSessionProperty(key, new SoftReference<Map<String, Externalizable>>(map));
      updateMapLRU(map);
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }
  }

  /**
   * Called to retrieve a map stored in the session property of the given
   * container.
   * 
   * @param container the container.
   * @param key the key of the property.
   * @return map stored in the property.
   * @throws PerforceException in case of an error.
   */
  private Map<String, Externalizable> getMapFromSessionProperty(IContainer container,
      QualifiedName key) throws PerforceException {
    try {
      @SuppressWarnings("unchecked")
      final SoftReference<Map<String, Externalizable>> ref =
          (SoftReference<Map<String, Externalizable>>) container.getSessionProperty(key);
      if (ref == null) {
        return null;
      }

      final Map<String, Externalizable> result = ref.get();
      updateMapLRU(result);

      return result;
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }
  }

  /**
   * Called to update the LRU list.
   * 
   * @param map the map to put into the LRU list.
   */
  private void updateMapLRU(Map<String, Externalizable> map) {
    if (map == null) {
      return;
    }

    lruMaps.remove(map);
    lruMaps.add(map);

    while (lruMaps.size() > LRU_THRESHOLD) {
      lruMaps.remove(0);
    }
  }

  /**
   * Called to retrieve the cached FolderSyncInfo for the given folder.
   * 
   * @param folder the folder.
   * @return return the FolderSyncInfo.
   * 
   * @throws PerforceException when reading of the persistent property fails.
   */
  private FolderSyncInfo getCachedFolderSync(IContainer folder) throws PerforceException {
    IContainer parent = folder.getParent();
    Map<String, Externalizable> children = getMapFromSessionProperty(parent, FOLDER_SYNC_KEY);
    Policy.assertNotNull(children);

    return (FolderSyncInfo) children.get(folder.getName());
  }

  /**
   * Method is called to retrieve the cached ResourceSyncInfo for the given
   * resource.
   * 
   * @param resource the resource.
   * @return the ResoruceSyncInfo.
   * 
   * @throws PerforceException when reading of the persistent property fails.
   */
  private ResourceSyncInfo getCachedResourceSync(IResource resource) throws PerforceException {
    IContainer parent = resource.getParent();
    Map<String, Externalizable> children = getMapFromSessionProperty(parent, RESOURCE_SYNC_KEY);
    Policy.assertNotNull(children);

    return (ResourceSyncInfo) children.get(resource.getName());
  }

  // ----------------------------------------------------------------------
  // Private Static Methods

  /**
   * Called to cache the ignore patterns for the given container.
   * 
   * @param container the container for which the ignores should be cached.
   * @return the cached patterns.
   * @throws PerforceException in case of an error.
   */
  private static String[] cacheFolderIgnores(IContainer container) throws PerforceException {
    try {
      String[] ignores = (String[]) container.getSessionProperty(IGNORE_SYNC_KEY);
      if (ignores == null) {
        ignores = MetaFileWriter.readP4IgnoreEntries(container);
        if (ignores == null) {
          ignores = NULL_IGNORES;
        }

        container.setSessionProperty(IGNORE_SYNC_KEY, ignores);
      }

      return ignores;
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }
  }

  /**
   * Sets the array of folder ignore patterns for the container, must not be
   * null. Folder must exist and must not be the workspace root.
   * 
   * @param container the container
   * @param ignores the array of ignore patterns
   */
  private static void setCachedFolderIgnores(IContainer container, String[] ignores)
      throws PerforceException {
    try {
      container.setSessionProperty(IGNORE_SYNC_KEY, ignores);
    } catch (CoreException e) {
      throw new PerforceException(e.getStatus());
    }
  }



}
